/*
 * Copyright 2011 Kim Lindhardt Madsen
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package dk.lindhardt.gwt.geie.shared;

import com.google.gwt.core.client.GWT;

import java.io.Serializable;
import java.util.*;

/**
 * Describes a layout of a table. Can also be used on client side or be sent over RPC.
 */
public class TableLayout implements Serializable {

   private static final long serialVersionUID = 6559564366738897253L;

   private ColumnCorrectionBookKeeper columnCorrectionBookKeeper;

   private List<Cell> cells;
   private List<Image> images;
   private Map<CellFormatKey, CellFormat> cellFormats;
   private Map<Position, Cell> cellsPositionMap;
   private Map<Integer, Integer> columnWidths;
   private Map<Integer, Integer> rowHeights;

   /**
    * New instance of a TableLayout
    */
   public TableLayout() {
      columnCorrectionBookKeeper = new ColumnCorrectionBookKeeper();
      cells = new ArrayList<Cell>();
      images = new ArrayList<Image>();
      cellFormats = new HashMap<CellFormatKey, CellFormat>();
      cellsPositionMap = new HashMap<Position, Cell>();
      columnWidths = new HashMap<Integer, Integer>();
      rowHeights = new HashMap<Integer, Integer>();
   }

   /**
    * Gets the column correction book keeper.
    * @return the column correction book keeper
    */
   public ColumnCorrectionBookKeeper getColumnCorrectionBookKeeper() {
      return columnCorrectionBookKeeper;
   }

   /**
    * Get the table cell using the specified point. If a cell spans rows or column and you use an index
    * in area that the cell spans you will still get this cell.
    *
    * @param row    row index
    * @param column column index
    * @return the table cell placed in this cell
    */
   public Cell getCell(int row, int column) {
      return getCell(row, column, PositioningType.EXCEL);
   }

   /**
    * Get the table cell using the specified point. If a cell spans rows or column and you use an index
    * in area that the cell spans you will still get this cell.
    *
    * @param row    row index
    * @param column column index
    * @param type type of positioning to use
    * @return the table cell placed in this cell
    */
   public Cell getCell(int row, int column, PositioningType type) {
      Position position;
      if (PositioningType.HTML.equals(type)) {
         position = new Position(row, column + columnCorrectionBookKeeper.getColumnCorrection(row, column, type));
      } else {
         position = new Position(row, column);
      }
      return cellsPositionMap.get(position);
   }

   /**
    * Get the tables cells sorted by columns and then rows
    *
    * @return cells of the table
    */
   public List<Cell> getCells() {
      Collections.sort(
            cells, new Comparator<Cell>() {
               public int compare(Cell c1, Cell c2) {
                  int column1 = c1.getPosition().getColumn();
                  int column2 = c2.getPosition().getColumn();
                  if (column1 > column2) {
                     return 1;
                  } else if (column1 < column2) {
                     return -1;
                  } else if (column1 == column2) {
                     int row1 = c1.getPosition().getRow();
                     int row2 = c2.getPosition().getRow();
                     if (row1 > row2) {
                        return 1;
                     } else if (row1 < row2) {
                        return -1;
                     }
                  }
                  return 0;
               }
            });
      return cells;
   }

   /**
    * Gets cells in specific row
    * @param row the index
    * @return list of cells in row
    */
   public List<Cell> getRow(int row) {
      List<Cell> result = new ArrayList<Cell>();
      List<Cell> cellsSortedOnRows = getCellsSortedOnRows();
      for (Cell cell : cellsSortedOnRows) {
         if (cell.getPosition().getRow() == row) {
            result.add(cell);
         } else if (cell.getPosition().getRow() > row) {
            break;
         }
      }
      return result;
   }

   /**
    * Gets cells in specific column
    * @param column the index
    * @return list of cells in column
    */
   public List<Cell> getColumn(int column) {
      List<Cell> result = new ArrayList<Cell>();
      List<Cell> cells = getCells();
      for (Cell cell : cells) {
         if (cell.getPosition().getColumn() == column) {
            result.add(cell);
         } else if (cell.getPosition().getColumn() > column) {
            break;
         }
      }
      return result;
   }

   /**
    * Add cells to table
    *
    * @param cells the cells to be added
    */
   public void addCells(Collection<Cell> cells) {
      for (Cell cell : cells) {
         addCell(cell);
      }
   }

   /**
    * Sets tables cells. This clears the cells this TableLayout may have had before.
    *
    * @param cells the cells
    */
   public void setCells(Collection<Cell> cells) {
      this.cells = new ArrayList<Cell>();
      this.cellsPositionMap = new HashMap<Position, Cell>();
      addCells(cells);
   }

   /**
    * Add cell to table
    *
    * @param cell the cell to be added
    */
   public void addCell(Cell cell) {
      cells.add(cell);

      Position position = cell.getPosition();
      columnCorrectionBookKeeper.register(position);

      Set<Position> filledSinglePositions = createFilledSinglePositions(position);
      for (Position singlePosition : filledSinglePositions) {
         cellsPositionMap.put(singlePosition, cell);
      }
   }

   /**
    * Returns cells in the rectangle between the two given cells.
    * @param pos1 first position
    * @param pos2 last position
    * @param type the positioning type of the given positions
    * @return a range of cells in a list
    */
   public List<Cell> getRange(final Position pos1, final Position pos2, PositioningType type) {

      int row2 = pos2.getRow();
      int col2;
      if (PositioningType.HTML.equals(type)) {
         col2 = pos2.getColumn() + columnCorrectionBookKeeper.getColumnCorrection(
               row2, pos2.getColumn(), PositioningType.HTML);
      } else {
         col2 = pos2.getColumn();
      }
      int row1 = pos1.getRow();
      int col1;
      if (PositioningType.HTML.equals(type)) {
         col1 = pos1.getColumn() + columnCorrectionBookKeeper.getColumnCorrection(
               row1, pos1.getColumn(), PositioningType.HTML);
      } else {
         col1 = pos1.getColumn();
      }

      int rowDiff = Math.abs(row2 - row1);
      int colDiff = Math.abs(col2 - col1);

      if (rowDiff == 0 && colDiff == 0) {
         return Arrays.asList(getCell(row1, col1, PositioningType.EXCEL));
      }

      int mostHighRow;
      int leftMostColumn;
      if (row1 <= row2) {
         mostHighRow = row1;
         if (col1 <= col2) {
            leftMostColumn = col1;
         } else {
            leftMostColumn = col2;
         }
      } else {
         mostHighRow = row2;
         if (col2 <= col1) {
            leftMostColumn = col2;
         } else {
            leftMostColumn = col1;
         }
      }

      List<Cell> result = createRectangle(mostHighRow, leftMostColumn, rowDiff, colDiff);
      setPositionInFront(pos1, result);

      return result;
   }

   /**
    * Sets the given position in front of the result
    * @param pos1 position to be in front
    * @param result the list
    */
   private void setPositionInFront(Position pos1, List<Cell> result) {
      Cell startCell = getCell(pos1.getRow(), pos1.getColumn(), PositioningType.HTML);
      int indexOfStartCell = result.indexOf(startCell);
      if (indexOfStartCell > 0) {
         Cell tempCell = result.set(0, startCell);
         result.set(indexOfStartCell, tempCell);
      }
   }

   /**
    * Rectangle algorithm. If a span is met that changes the rectangle the limits is changed and another rectangle is
    * being build.
    * @param startRow start row
    * @param startCol start col
    * @param rowDiff  difference from startRow to row limit
    * @param colDiff  difference from startCol to column limit
    * @return the rectangle
    */
   private List<Cell> createRectangle(
         int startRow, int startCol, int rowDiff, int colDiff) {
      int rowLimit = startRow + rowDiff;
      int colLimit = startCol + colDiff;
      List<Cell> result = new ArrayList<Cell>();
      for (int r = 0; r <= Math.abs(rowDiff); r++) {
         for (int c = 0; c <= Math.abs(colDiff); c++) {
            int row = startRow + r;
            int column = startCol + c;
            Cell cell = getCell(row, column);
            if (cell != null) {
               Position position = cell.getPosition();
               if (position.getRow() < startRow) {
                  int newStartRow = position.getRow();
                  return createRectangle(newStartRow, startCol, rowDiff + (startRow - newStartRow), colDiff);
               }
               if (position.getColumn() < startCol) {
                  int newStartColumn = position.getColumn();
                  return createRectangle(row, newStartColumn, rowDiff, colDiff + (startCol - newStartColumn));
               }
               if (position.getSpanRow() > 1 && !result.contains(cell)) {
                  if (row + (position.getSpanRow() - 1) > rowLimit) {
                     return createRectangle(
                           startRow,
                           startCol,
                           rowDiff + (row - rowLimit) + (position.getSpanRow() - 1),
                           colDiff);
                  }
               }
               if (position.getSpanColumn() > 1 && !result.contains(cell)) {
                  int cellColumnLimit = column + (position.getSpanColumn() - 1);
                  if (cellColumnLimit > colLimit) {
                     return createRectangle(
                           startRow,
                           startCol,
                           rowDiff,
                           colDiff + column + position.getSpanColumn() - 1 - colLimit);
                  }
               }
               if (!result.contains(cell)) {
                  result.add(cell);
               }
            }
         }
      }
      return result;
   }

   /**
    * Replaces the cell given in first parameter with the cell in the second parameter. Returns <code>true</code> if cell
    * was successfully replaced and <code>false</code> otherwise. If the two cells has disjoint positions the old cell
    * is removed and the new one put on its own position. This can overwrite some other cells and cause table not to look
    * as expected, so think about it before you do that.
    *
    * @param oldCell cell to be replaced
    * @param newCell cell to be inserted
    * @return if successful
    */
   public boolean replaceCell(Cell oldCell, Cell newCell) {
      int index = cells.indexOf(oldCell);
      if (index == -1) {
         GWT.log("Warning: The old cell did not exist in the table and is therefore not replaced.");
         return false;
      }

      Position oldPosition = oldCell.getPosition();
      columnCorrectionBookKeeper.unRegister(oldPosition);
      Set<Position> oldSinglePositions = createFilledSinglePositions(oldPosition);
      for (Position oldSinglePosition : oldSinglePositions) {
         cellsPositionMap.remove(oldSinglePosition);
      }

      Position newPosition = newCell.getPosition();
      columnCorrectionBookKeeper.register(newPosition);

      Set<Position> newSinglePositions = createFilledSinglePositions(newPosition);
      for (Position singlePosition : newSinglePositions) {
         if (cellsPositionMap.get(singlePosition) != null) {
            GWT.log(
                  "Warning: Overwriting some other cell than " + oldCell + " on " + singlePosition.toString() + ".");
         }
         cellsPositionMap.put(singlePosition, newCell);
      }
      return cells.set(index, newCell) != null;
   }

   /**
    * Get all cell formats for this table
    *
    * @return a map[key of format, format]
    */
   public Map<CellFormatKey, CellFormat> getCellFormats() {
      return cellFormats;
   }

   /**
    * Gets the cell format with the given key
    *
    * @param key the key of the cell format
    * @return the cell format
    */
   public CellFormat getCellFormat(CellFormatKey key) {
      return cellFormats.get(key);
   }

   /**
    * Add a cell format which then is available to be used by the tables cells
    *
    * @param key            give the format a unique name
    * @param cellFormat     the cell format
    * @return key of the new format
    */
   public CellFormatKey addCellFormat(String key, CellFormat cellFormat) {
      CellFormatKey cellFormatKey = new CellFormatKey(key);
      cellFormats.put(cellFormatKey, cellFormat);
      return cellFormatKey;
   }

   /**
    * Add a cell format which then is available to be used by the tables cells
    *
    * @param key            key of the format
    * @param cellFormat     the cell format
    */
   public void addCellFormat(CellFormatKey key, CellFormat cellFormat) {
      cellFormats.put(key, cellFormat);
   }

   /**
    * Shifts all column to the right of this index to the right.
    *
    * @param index the index of the new column
    */
   public void addColumn(int index) {
      for (Cell cell : cells) {
         Position position = cell.getPosition();
         int column = position.getColumn();
         if (column > index) {
            position.setColumn(column + 1);
         }
      }
      setCells(new ArrayList<Cell>(cells));
   }

   /**
    * Shifts all rows after this index downwards.
    *
    * @param index the index of the new row
    */
   public void addRow(int index) {
      for (Cell cell : cells) {
         Position position = cell.getPosition();
         int row = position.getRow();
         if (row > index) {
            position.setRow(row + 1);
         }
      }
      setCells(new ArrayList<Cell>(cells));
   }

   /**
    * @deprecated this is wrong
    * @return Gets number of columns
    */
   public int columns() {
      Cell lastCell = getCells().get(getCells().size() - 1);
      return lastCell.getPosition().getColumn() + 1;
   }

   /**
    * @return Gets number of rows
    */
   public int rows() {
      List<Cell> tempCells = getCellsSortedOnRows();
      Cell lastCell = tempCells.get(tempCells.size() - 1);
      return lastCell.getPosition().getRow() + 1;
   }

   /**
    * @return Column widths in pixels as a map(colIndex, width)
    */
   public Map<Integer, Integer> getColumnWidths() {
      return columnWidths;
   }

   /**
    * Set width of specific column
    *
    * @param columnIndex index of column
    * @param width       width in pixels
    */
   public void setColumnWidth(int columnIndex, int width) {
      columnWidths.put(columnIndex, width);
   }

   /**
    * @return Row heights in pixels as a map(rowIndex, height)
    */
   public Map<Integer, Integer> getRowHeights() {
      return rowHeights;
   }

   /**
    * Set height of specific row
    *
    * @param rowIndex the index of the row
    * @param height   height in pixel
    */
   public void setRowHeights(int rowIndex, int height) {
      rowHeights.put(rowIndex, height);
   }

   /**
    * @param cellFormats sets formats available to this table
    */
   public void setCellFormats(Map<CellFormatKey, CellFormat> cellFormats) {
      this.cellFormats = cellFormats;
   }

   /**
    * @param columnWidths in pixels as a map(colIndex, width)
    */
   public void setColumnWidths(Map<Integer, Integer> columnWidths) {
      this.columnWidths = columnWidths;
   }

   /**
    * @param rowHeights in pixels as a map(rowIndex, height)
    */
   public void setRowHeights(Map<Integer, Integer> rowHeights) {
      this.rowHeights = rowHeights;
   }

   /**
    * @return Gets cells sorted on rows and then columns
    */
   List<Cell> getCellsSortedOnRows() {
      List<Cell> cellsSortedOnRows = new ArrayList<Cell>(cells);
      Collections.sort(
            cellsSortedOnRows, new Comparator<Cell>() {
               public int compare(Cell c1, Cell c2) {
                  if (c1.getPosition().getRow() > c2.getPosition().getRow()) {
                     return 1;
                  } else if (c1.getPosition().getRow() < c2.getPosition().getRow()) {
                     return -1;
                  } else if (c1.getPosition().getRow() == c2.getPosition().getRow()) {
                     if (c1.getPosition().getColumn() > c2.getPosition().getColumn()) {
                        return 1;
                     } else if (c1.getPosition().getColumn() < c2.getPosition().getColumn()) {
                        return -1;
                     }
                  }
                  return 0;
               }
            });
      return cellsSortedOnRows;
   }

   /**
    * Returns image for this table layout
    * @return images listed
    */
   public List<Image> getImages() {
      return images;
   }

   /**
    * Sets images for this table layout
    * @param images the images
    */
   public void setImages(List<Image> images) {
      this.images = images;
   }

   /**
    * Adds a image to this table layout
    * @param image the image
    */
   public void addImage(Image image) {
      images.add(image);
   }

   /**
    * Creates single filled positions from a position. If position given contains span fx example (0, 0, 0, 2) it will
    * return a set containing [(0,0), (0,1)]. It creates a set of the spanned cells.
    * @param pos the position
    * @return set of spanned cells as single positions
    */
   private Set<Position> createFilledSinglePositions(Position pos) {
      Set<Position> filledSinglePositions = new HashSet<Position>();
      if (pos.isHasSpans()) {
         Integer columnSpan = pos.getSpanColumn();
         Integer rowSpan = pos.getSpanRow();
         if (rowSpan > 1 && columnSpan > 1) {
            for (int r = 0; r < rowSpan; r++) {
               for (int c = 0; c < columnSpan; c++) {
                  filledSinglePositions.add(new Position(pos.getRow() + r, pos.getColumn() + c));
               }
            }
         } else if (rowSpan > 1) {
            for (int r = 0; r < rowSpan; r++) {
               filledSinglePositions.add(new Position(pos.getRow() + r, pos.getColumn()));
            }
         } else if (columnSpan > 1) {
            for (int c = 0; c < columnSpan; c++) {
               filledSinglePositions.add(new Position(pos.getRow(), pos.getColumn() + c));
            }
         }
      } else {
         filledSinglePositions.add(pos);
      }
      return filledSinglePositions;
   }
}
